Сейчас я буду убеждать вас использовать статический анализ в PHP



    Я помню выход PHP7: появились strict types, скалярные type hint-ы.

    Мы начали двигаться в сторону языка со статической типизацией, но типизация не ушла в статику. Концептуально все осталась прежним — мы запускаем программу и только в runtime узнаем, что где-то есть неправильный тип. Даже если мы везде явно проставим типы, все ошибки мы не поймаем — и можем больно упасть в продакшене.

    Всем привет! Это частичная расшифровка моего подкаста «‎Между скобок»‎ — разговоров обо всем, что связано с разработкой на PHP. В гостях у меня Валентин Удальцов, автор телеграм-канала «‎Пых»‎, который регулярно пишет о плюсах статического анализа.

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

    Если вы решите углубиться в тему, советую эти видео:


    Поехали!


    Сергей Жук, Skyeng: Что вообще анализируют статические анализаторы? Они же не только про типы, да?

    Валентин Удальцов, «‎Пых»‎: Статический анализ выходит далеко за пределы декларации типов в PHP. Типизация может быть очень многогранна: скажем, это может быть работа с иммутабельность и чистотой. То есть, мы можем гарантировать, что объект определенного класса не меняется в процессе его использования, на нем нет setter-ов.

    Мы также можем следить за чистотой функции: смотреть, что она не обращается к IO.

    В Psalm еще недавно добавили taint analysis: мы можем проследить данные от источника из внешней среды до какого-то уровня вглубь и понять, что все данные были так или иначе провалидированы. Тогда, с некоторой долей вероятности, у нас нет проблем с безопасностью. Вот еще одна задача, которую может решать статический анализ.

    Сергей Жук, Skyeng: А ты вообще разбирался, как это работает? Если мой код не выполняется, получается, он парсится в абстрактное синтаксическое дерево и...

    Валентин Удальцов, «‎Пых»‎: Есть статические анализаторы, которые используют autoloading композеровский — те же PHPStan и Psalm. Phan — по-моему, частично использует runtime, но дальше сам анализ происходит без выполнения.

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

    Есть два этапа. На первом Psalm проходит весь код, который ты указал ему явно полностью. Традиционно это папка src и любые другие, которые ты перечислил. Он также заглядывает в vendor, если твой код использует вендорные классы. Это просто сбор данных: он все кэширует, трансформируя данные парсера Попова в некоторые свои сущности.

    Потом, при следующих проходах, первый этап повторяется только для тех файлов и зависимостей, которые поменялись. За счёт этого Psalm работает достаточно быстро.

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

    Сергей Жук, Skyeng: Ты постоянно упоминаешь Psalm — а почему не другие?

    Валентин Удальцов, «‎Пых»‎: Да все просто. Когда я вообще что-то выбирал, то договорился на работе в Happy Inc., что начинаю новый проект и мы сразу включаем всё на максимум. Посмотрел, что чаще упоминают, сделал базовое сравнение фич. Phan не использовал composer autoloading, насколько помню. В PHPStan было больше плагинов. В Psalm была иммутабельность, еще какие-то вещи, частые релизы, сообщество, — а человек, который его развивает, на оплачиваемой должности. Все это подкупило.

    Выбор я сделал и не слежу, как развиваются альтернативы. Просто времени нет. Но Psalm до сих пор часто удивляет: скажем, с новым релизом может появить фича, про которую я знать не знал, что это можно проверять статически.

    Бывает и так, что выйдет новый релиз, а там какая-то регрессия или на что-то не написан тест.

    Естественно, приходится такое suppress-ить, писать issue. Мой лайфхак: если вы нашли проблему в любом пакете, который ставите, напишите todo, создайте issue, а в todo впишите ссылку на issue. Когда issue закроют, по его url вы найдете все места в коде, где нужно убрать suppress.

    С Psalm я так и поступаю: не откатываю версию, я просто пишу suppress-ы и обязательно делаю тикет.

    Сергей Жук, Skyeng: А как подсадить на статанализ команду? Люди ждут, что им сейчас всякие хитрые баги найдут, а в реальности все немного иначе.

    Валентин Удальцов, «‎Пых»‎: У меня нет опыта внедрения Psalm на большом проекте с кучей людей. Этот момент я признаю. Мне помогло то, что я начинал писать проект один, хотел по полной обмазываться статическим анализом, — и сделал это. А когда начали подключаться люди с других проектов, они были поставлены в существующие рамки. Конечно, поначалу я объяснял и показывал, но у нас небольшой техотдел. Так что не сказал бы, что адаптация других ребят заняла много времени.

    Для тех, у кого много людей и старая кодовая база, в инструментах статического анализа есть такая история, как baseline.

    Когда ты добавляешь инструмент в старый проект, там найдется миллион ошибок. Но ты говоришь анализатору: мы признаем, что это есть, но пока не обращаем на это внимание — а вот когда мы пишем новое, рассказывай, где проблемы. Это baseline — и постепенно, относительно него, разработчики учатся писать код с учетом статического анализа. При этом в Psalm есть несколько уровней, то есть инструмент максимально заточен, чтобы внедрять его постепенно.

    А потом, когда все привыкли, можно постепенно рефакторить. Ну либо не рефакторить, но хотя бы новый код писать с учетом статанализа.

    Сергей Жук, Skyeng: Бывает, анализаторы ругаются на неверные PHP-доки, хотя сам код работает правильно. Да и ты говорил про баги, которые помечаешь todo-шками… Есть ли тогда смысл добавлять статанализ наравне с тестами в CI?

    Валентин Удальцов, «‎Пых»‎: Добавлять надо, иначе теряется смысл. Идея типизации в том, чтобы снизить энтропию, повысить детерминированность того, что мы пишем. А богатая типизация позволяет писать меньше тестов. Если мы знаем, что где-то есть тип, который возвращает в этом случае null, а в этом string, если мы точно знаем, в каких случаях что возвращается, то не понадобится писать тест, который это проверяет.

    Статический анализ это полноценный этап CI, который пробегает до тестов.

    Если у тебя типы неверные, что там тестировать? Ты в любом случае накосячил.

    Что касается багов в Psalm, обычно это минорные вещи, не самая большая проблема. Если есть реальная проблема, мы формируем issue и костыльком в виде suppress или подсказки с todo-шкой это фиксим. При этом бывают нюансы, которые ты сразу не понимаешь, — делаешь issue, а тебе отвечают: «‎Да ты просто здесь вот это не так указал»‎. И скидывают как правильно.

    Сергей Жук, Skyeng: Ок, но для таких инструментов свой синтаксис надо учить. И твой код им обрастет. Как ты думаешь, в будущем это всё мигрирует в сам PHP?

    Валентин Удальцов, «‎Пых»‎: Я смотрю на свой текущий проект. Он написан как бы не на PHP, а на PHP плюс Psalm. Но это всё-таки далеко от того, чтобы быть прям совсем другим языком. Это не как Haskell и PHP, например.

    Что касается будущего, трудно сказать. Если код не типизирован, вероятность ошибки выше. Пока PHP как бы на двух стульях сидит: с одной стороны, позволяет mixed (и это иногда удобно), а с другой, мы так или иначе стремимся к типизации.

    Сергей Жук, Skyeng: Ок, а что бы ты сказал человеку, который сейчас открыл IDE, создал новый проект и думает, добавлять статанализ — или ну его?

    Валентин Удальцов, «‎Пых»‎: Бизнес-логику, естественно, никакой статический анализ не проверит. А вот типы и все эти совершенные тупые ошибки — снижаются процентов на 80. Да, в начале будет сложнее. Но в долгосрочной перспективе это профит. Статический анализ передвигает исправление багов ближе к началу разработки, а это снижает издержки на поддержку проекта.

    Ведь всех бесят ошибки с продакшена, когда вместо expected string пришел int, null где-то появился или undefined index. Поэтому разработчик может взвесить все плюсы и минусы, понять, что благодаря статическому анализу вот такие совершенно глупые ошибки исключаются. А еще есть приятный бонус в виде дженериков.

    Сергей Жук, Skyeng: А всё же есть какие-то минусы, каких-то кейсы, где вообще не надо использовать стат анализ?

    Валентин Удальцов, «‎Пых»‎: Если проект реально большой, а команда привыкла полагаться на runtime — издержки на интеграцию могут быть запредельными. Но все равно я бы советовал попробовать baseline внедрить, посмотреть, что как будет. Многие вещи надо в несколько этапов внедрять.

    А если вы распиливаете большой монолитный сервис, и по какой-то причине продолжаете на PHP все делать, стоит попробовать.

    Это не только хайп последних лет. Это реально полезно. Если мы полагаемся на runtime, то обязательно наткнемся на сложную ситуацию, что и поправить безумно дорого, и клиент готов уйти, и всё-всё очень плохо.

    p.s. Найти другие выпуски подкаста вы можете здесь.
    Skyeng
    Крупнейшая онлайн-школа Европы. Удаленная работа

    Комментарии 18

      +1
      Куча народа даже плагин Php Inspections ​(EA Extended) не ставят, не то что какие-то сторонние анализаторы, которые ещё и настраивать нужно (
        0

        Куча народа даже с компьютером не в ладах, не то что какие-то IDE которые ещё и установить нужно :)


        В целом согласен, но может быть просвещение как раз и поможет. К тому в следующей версии PhpStorm, анализаторы поддерживаются из коробки.

          +1
          Спросил у знакомого с крупной компании: «а что ты используешь?», а он ошарашил: «Notepad++». Я очень удивился, спросил: «эм, а как же IDE, подсветка измененных строк, переход к объявлению функции, автодополнение методов?» ответил: «а зачем?».
          Спросил о тестировании, деплое: «ну мы что-то там думали тестить, но мы этим не заморачиваемся». И это, кстати, в одном из крупнейших агрегаторов цен Украины.
          А вы о каких-то анализаторах говорите…
            0

            Php Inspections — тоже сторонний анализатор, который ещё и настраивать нужно. Смотрю на его вывод в окне редактирования, только если несколько ошибок в файлк подсвечивает, а не сотни. Если в проекте активно используется динамическая "магия" PHP, то усилия на поддержку статанализаторов, имхо, не окупаются. Проще проект полностью переписать с нуля, отказавшись от магии в нём и/или библиотек/фреймворков, её использующих (привет, моя боль Laravel+Eloquent), чем статически объявить эту магию в аннотациях и держать её актуальной.

            0

            Как-то пробовал внедрить psalm на проекте на базе Laravel. Практически нереально из-за кучи магии. Там даже с обычным phpdoc проблемы, в IDE всё красное "method/property not found in class Model" и т. п., а с каким-то сторонним пакетом типа ide-support (которому ещ' и база запущенная нужна) красное из ошибок множественніх деклараций и навигация сломана

              0
              Там даже с обычным phpdoc проблемы, в IDE всё красное

              Для Laravel-а практически обязателен https://github.com/barryvdh/laravel-ide-helper, который решает почти все проблемы с автокоплитом в PHPStorm-е :) (быстрее бы еще фабрики смержили...)

                0

                Он, насколько я помню, требует доступа к базе при разработке (или локально, или открытой извне, в ssh-тонелли не умеет). И, главное, лишь уменьшает количество "красного", в частности провоцируя ошибки типа "в проекте несколько деклараций класса User", и ухудшая UX для, например, навигации: приходится вібирать на метод какого User мі сейчас переходим.

                  0

                  Не, он ничего не требует, кроме, разве что PHPStorm-а, для которого он генерирует метадату (автокоплит для контейнера и переопределение типа возвращаемого значения некоторых методов). Дополнительно к ней он может создавать псевдоклассы для фасадов и Macroable, ну и генерировать phpdoc с полями для моделей (базу при этом использует туже что и сама модель).

                    0
                    ну и генерировать phpdoc с полями для моделей (базу при этом использует туже что и сама модель).

                    так модели это же самое главное, что от него требуется. И ладно поля — их можно ручками прописать, а вот методы, особенно статически-магические...

                      0

                      Для моделей он вот такое генерит:


                      /**
                       * App\Models\Site
                       *
                       * @property int    $id
                       * @property string $type
                       * @property string $title
                       * @property string $email
                       * @property string $url
                       * @method static \Illuminate\Database\Eloquent\Builder|Site newModelQuery()
                       * @method static \Illuminate\Database\Eloquent\Builder|Site newQuery()
                       * @method static \Illuminate\Database\Eloquent\Builder|Site query()
                       * @method static \Illuminate\Database\Eloquent\Builder|Site whereEmail($value)
                       * @method static \Illuminate\Database\Eloquent\Builder|Site whereId($value)
                       * @method static \Illuminate\Database\Eloquent\Builder|Site whereThrottle($value)
                       * @method static \Illuminate\Database\Eloquent\Builder|Site whereTitle($value)
                       * @method static \Illuminate\Database\Eloquent\Builder|Site whereType($value)
                       * @method static \Illuminate\Database\Eloquent\Builder|Site whereUrl($value)
                       * @mixin \Eloquent
                       */
                      class Site extends Model {}
                        0

                        Но для этого ему нужен доступ к базе на момент генерации. Пробовали делать в CI — не понравилось.

                          0
                          Но для этого ему нужен доступ к базе на момент генерации.

                          Так это логично, как иначе он узнает какие поля есть у таблицы?

                            0

                            Миграции может проанализировать. )


                            А если серьёзно, то вот об этом я и говорю: какой может быть статический анализ, если Ларавел кучу рантайм информации о типах формирует в рантайме.

                              0
                              Миграции может проанализировать. )

                              Ну… у меня оно raw sql например)

                                0

                                sql сервер же из них таблички собирает как-то )) Дп и у того же jetbrains статанализатор sql


                                А вообще костыли всё это. Ладно, нужен нам статанлиз. Так выбирать нужно если не язык, то фреймворк и архитектуру, которая минимально динамичны, а не сплошная магия, которую ни вручную, ни по исходникам не отследишь без живой и актуальной базы.

                                  0
                                  статанализатор sql

                                  Проблема в том что Laravel из коробки использует php для миграций, но оно поддерживает далеко не всё что есть в sql (из совсем банального — нету енумов), поэтому в реальности миграции все равно содержат sql, который еще и привязан к конкретной базе (собственно это одна из основных причин полного перехода на raw sql).


                                  А вообще костыли всё это.

                                  Ага, но ide-helper сильно всё упрощает :) И кстати, вон вроде плагин есть, который под капотом использует ide-helper (сам не пробовал).

                                    0
                                    Ага, но ide-helper сильно всё упрощает :)

                                    Создаём себе трудности и героически их преодолеваем пакетами и плагинами :) Надо бы попробовать ещё раз, может Makefile какой сделать, который будет в докере базу поднимать и вотчер запускать, который будет миграции мониторить, накатывать их, а потом ide-helper запускать. Или CI настроить так чтобы она на коммит в репу, запускала ide-helper и коммитила в эту же репу в эту ветку. Надо только убедиться, что зацткливаться не будет.

              0
              Удобнее ли использовать SonarQube с учетом того, что статического кода не всегда достаточно, а ставить кучу отдельных свистелок и интегрировать в CI их неудобно?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое