Избавляемся от рутины со своим плагином для PhpStorm

Привет, Хабр! Я тружусь в команде Антиспама, и, как и у большинства бэкенд-разработчиков Badoo, большая часть времени у меня уходит на работу с PHP-кодом.

С этой работой связано много специфических проблем и задач, которые можно решить или упростить. Когда мне надоело вручную делать то, что может делать за меня IDE, я решил попробовать доработать самую распространённую в компании IDE — PhpStorm — и написать плагин, решающий рутинные задачи, упростив тем самым жизнь себе и коллегам.

Так появился плагин Badoo для PhpStorm, который мы сегодня активно используем. За несколько лет его возможности серьёзно расширились, мы его развиваем, и в этой статье я расскажу на примере наших кейсов, как адаптировать IDE под свои задачи и инструменты, и докажу, что это не так сложно, как кажется.



Все знают, что JetBrains разрабатывает коммерческие продукты, однако на тот момент для меня стало открытием, что IDEA — это open-source-проект. По сути, это платформа для построения IDE, а все среды JetBrains являются наборами плагинов, специализирующихся на решении задач определённого языка программирования. Отсюда пришло осознание, что весь функционал, который реализует PhpStorm, можно заточить под собственные нужды. К слову, Eclipse использует аналогичный подход.

Из этого следует, что все трюки, которые умеет делать PhpStorm, можно использовать для создания более специализированной среды, ещё сильнее облегчающей жизнь. В Badoo мы используем много собственных инструментов и стараемся сделать разработку максимально комфортной, поэтому идея написать собственный плагин нам показалась как минимум стоящей эксперимента.

Не буду заострять внимание на том, как писать плагины: на Хабре уже были статьи об этом, на сайте есть QuickStart, да и в целом достаточно скачать шаблон тут (в нашем случае подойдёт Simple). Если вы считаете, что тема раскрыта не до конца, скажите об этом в комментариях — я напишу более подробную статью об этом.

Итак, давайте рассмотрим на примерах, для каких кейсов и как мы дорабатывали PhpStorm.

Валидация кастомного кода


В PHP есть функции типа printf, когда первым аргументом передаётся шаблон, а дальше мы передаём аргументы, которые будут подставлены в этот шаблон. Однако стандартные инспекции PhpStorm проверяют корректность только в случае встроенных в PHP функций. Поэтому мы добавили аналогичные проверки наших функций логирования.

Например, у нас есть свой Logger, у которого есть методы infof, errorf и т. д. Мы пишем плагин, который будет валидировать, что количество аргументов соответствует количеству подстановок в шаблоне.

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

Создаём класс-наследник LocalInspectionTool:

class LoggerFormatInspection : LocalInspectionTool() {
   override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
       if (session.file !is PhpFile) {
           // Нас не интересуют не-PHP-код
           return super.buildVisitor(holder, isOnTheFly, session)
       }
       return LoggerFormatVisitor(holder, isOnTheFly)
   }
}


Основная работа будет происходить в классе LoggerFormatVisitor. По сути, он пробегается по всем элементам исходного кода (PsiElement, PSI — Program Structure Interface) и вызывает для них обработчик. Метод buildVisitor будет запускаться для каждого файла в проекте.

class LoggerFormatVisitor(private val holder: ProblemsHolder, onTheFly: Boolean) : PsiElementVisitor() {
   override fun visitElement(element: PsiElement) {
       if (!isLoggerFormatFunction(element)) {
           return // Пропускаем всё, что не похоже на функцию логгера
       }

       checkMethodReference(element as MethodReference) //
   }
}

Первый метод (isLoggerFormatFunction()) убеждается, что текущий элемент — это вызов функции, и проверяет сигнатуру метода: переменная-логгер должна быть наследником класса \Logger\Logger, а имя метода должно быть одним из тех, что поддерживают параметры prinf-like функций (у нас таких шесть: debugf, infof, noticef, warningf, errorf, infof).

Второй метод делает непосредственно работу: проверяет параметры и подсвечивает ошибки.

Давайте его и разберём.

private fun checkMethodReference(element : MethodReference) {
   // Сигнатура printf — следующая:
   // Первый (нулевой) аргумент — формат-строка; если таковой нет, то выходим из функции в расчёте на то, что программист ещё не дописал её (хотя это решение спорно)
   // Начиная со второго идут аргументы формата, их-то мы и будем проверять на соответствие
   val printfArgument = 0
   val firstPossibleArgument = printfArgument + 1
   if (element.parameters.size <= printfArgument) {
       return
   }

   val formatLine = element.parameters[printfArgument] as? StringLiteralExpression // Если первый аргумент не строка, выходим
           ?: return

   val expectingParameters = getExpectingParameters(formatLine)
   val arguments = element.parameters.slice(IntRange(firstPossibleArgument, element.parameters.size - 1))

Мы нашли первый аргумент — формат-строку и распарсили его (я опущу код парсинга, его можно найти в финальном исходнике). expectingParameters теперь содержит описание параметров, которые ожидаются в последующих аргументах. А в arguments как раз находятся аргументы, которые мы будем проверять.

Остаётся написать код проверки!

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

if (expectingParameters.isEmpty() && arguments.isNotEmpty()) {
   holder.registerProblem(arguments.first(), "No format item found in first parameter but call has more than one argument", ProblemHighlightType.WARNING)
   ++ problems
}

В редакторе будет подсвечен первый неописанный аргумент с критичностью warning.
Число ожидаемых аргументов может не соответствовать числу переданных (в обе стороны).

if (expectingParameters.isNotEmpty()) {
   var expectingIndex = 0
   for (i in arguments.indices) {
       if (expectingParameters.size <= expectingIndex) {
           // Если аргументов больше, чем в формат-строке, подсвечиваем лишние аргументы
           holder.registerProblem(
                   arguments[i],
                   "Format line expecting only ${expectingParameters.size} parameters",
                   ProblemHighlightType.WARNING
           )
           ++ problems
           continue
       }
       ++ expectingIndex
   }

   if (arguments.size < expectingParameters.size) {
       for (i in arguments.size until expectingParameters.size) {
           val item = expectingParameters[i]
           // Если нашли неиспользованный плейсхолдер, помечаем это как warning
           holder.registerProblem(
                   formatLine,
                   "Unused format item",
                   ProblemHighlightType.WARNING,
                   TextRange(item.rangeStart + 1, item.rangeEnd + 2)
           )
           ++ problems
       }
   }
}

Первый аргумент в holder.registerProblem() — это элемент PSI, который мы считаем ошибочным. Параметры кажутся очевидными, поэтому не буду на них останавливаться.

Внимательный читатель заметит, что во втором случае есть ещё какой-то TextRange с непонятными манипуляциями числами. Структура item содержит в себе место в тексте формат-строки, где мы обнаружили плейсхолдер. Однако элемент formatLine, помимо содержимого строки, содержит обрамляющие её кавычки. Чтобы найти местоположение плейсхолдеров, мы добавляем единицу, чтобы скорректировать смещение. Как вы уже поняли, аргумент TextRange нужен, чтобы подсвечивать не целиком формат-строку, а лишь ту её часть, где располагается плейсхолдер.

И последнее: если ошибка всё-таки обнаружена, покажем это более явно, подсветив имя функции.

if (problems > 0) {
   val elementStart = element.textOffset
   val nameNodeTextRange = element.nameNode!!.textRange

   val nameTextRange = TextRange(nameNodeTextRange.startOffset - elementStart, nameNodeTextRange.endOffset - elementStart)
   holder.registerProblem(element, nameTextRange, "Invalid format function usage")
}

И снова манипуляции со смещениями. Суть их в том, что нам надо указать смещение внутри элемента MethodReference (обращение к методу). Он, в свою очередь, состоит из объекта, оператора «стрелочка», имени вызываемого метода и аргументов. Для большей аккуратности мы маркируем только имя функции. Также можно заметить, что тут не указана критичность ошибки — она будет взята из настройки инспекции (настройки PhpStorm -> Editor -> Inspections).
На этом всё, фича готова.



Полностью исходник инспекции можно посмотреть тут.

Далее я расскажу о некоторых интересных возможностях, которые мы реализовали, но не буду подробно останавливаться на их реализации.

Работа с базами данных


По историческим причинам у нас есть своя обёртка для работы с базой, которая на вход принимает шаблон с SQL-запросом и массив с именованными подстановками. Это ещё один пример использования инспекций: мы валидируем, что в массиве с параметрами присутствуют все необходимые подстановки и нет лишних.



Как видите, помимо стандартных конструкций SQL, в запросе присутствуют плейсхолдеры, для которых разработчик должен указать подстановки. Во время написания кода мы добавили в автодополнение известные плейсхолдеры (это ключи массива) и проверку на предмет того, все ли они указаны.

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



Кстати, заметили, что запрос указан в константе класса? Несмотря на это, инспекция будет работать: мы проходимся по всем элементам параметра sql, вычисляем значения аргументов и выполняем конкатенацию, если это требуется. Поэтому не имеет значения, где написан запрос — все параметры будут заполнены правильно.

Кроме того, у нас имеется более сотни MySQL-серверов. Мы собираем данные о базах и таблицах на всех машинах и можем проверить, правильный ли коннект используется в запросе (этот функционал в данный момент находится в разработке).

Генерация boilerplate-кода


Во многих фреймворках есть функционал, который пишется по какому-то шаблону, и во многих местах проекта надо писать примерно одинаковый код. Пример — модули в Yii.

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

Например, если в фасаде модуля определить функцию, PhpStorm предложит Quick Fix для создания соответствующего класса.



Он заполнит тело функции, создаст необходимый класс-обработчик запроса и выполнит необходимые действия для его регистрации в системе.

public function onAcceptRequest(\Framework\Request $request, \User $user): void
{
   $command = new Commands\OnAcceptRequest($request, $user);
   return $command->run();
}

Пример класса:

<?php
namespace Modules\Chaos\Commands;

class OnAcceptRequest extends \Modules\core\AbstractCommand
{
   private $request;

   private $user;

   public function __construct(\Framework\Request $request, \User $user)
   {
       $this->request = $request;
       $this->user = $user;
   }

   protected function execute()
   {
       // TODO implement me
   }
}

Поддержка SoftMocks


Ещё одной особенностью нашего фреймворка является активное использование SoftMocks в тестах. Например:

\Badoo\SoftMocks::redefineMethod(\User\ProfilePhoto::class, ‘getAll’, $arguments, $code)

В этом фрагменте кода мы перехватываем вызовы метода \User\ProfilePhoto::getAll(), чтобы избежать хождения в базу из тестов. Это не является вызовом метода с точки зрения синтаксиса языка, однако мы знаем эту особенность фреймворка, поэтому научили нашу среду распознавать эту строку как обращение к соответствующему методу (что включает в себя навигацию, отображение в Find Usages и автодополнение второго параметра).

Отладка тестов (удалённая и с SoftMocks!)


Помимо написания тестов, в обязанности разработчика часто входит их отладка. Иногда она производится на удалённом сервере. Задача усложняется тем, что SoftMocks переписывают исходный текст и интерпретатор PHP выполняет не тот код, который отображается в IDE (хотя и равнозначный). А так как путь к исполняемому файлу определяется динамически, стандартный Path Mapping в PhpStorm оказывается бессилен и не может соотнести путь на удалённом сервере с путём в открытом проекте.

Используя стандартные средства, с этой задачей можно справится так:

  1. Установить Xdebug proxy.
  2. Настроить тестовый фреймворк PhpStorm для запуска на удалённой машине (подробнее об этом можно почитать тут).
  3. Настроить Path Mapping для проекта.
  4. Молиться, чтобы всё было выполнено без ошибок.

Можно также забить на интерактивную отладку и дебажить с помощью print (вполне действенный способ).

Учитывая востребованность данной процедуры, мы научили IDE делать всё самостоятельно: поддерживать динамическое отображение путей, подключение по SSH, запуск тестов и собственно отладку.

Подтягиваем статистику


Мы внимательно следим за производительностью нашего кода. Так, у нас на серверах активно используется Liveprof (ранее мы рассказывали об этом). Он отслеживает изменение производительности кода и позволяет отследить момент, когда что-то пошло не так.

Собранную статистику мы также импортируем в IDE, чтобы можно было быстро получить к ней доступ. Этот функционал доступен в виде отдельного плагина. Непосредственно в IDE отображается статистика обращений к методу за последние сутки с разбивкой по приложениям, которые его использовали.



Отображаем легаси


На последнем PHP Meetup мы рассказывали про сбор мёртвого кода.

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



Кстати, заметили, что массив подчёркнут как ошибка? Это другая наша инспекция, она подсказывает, что массив плейсхолдеров заполнен не до конца. Кажется, что этот код действительно не выполняется, иначе бы ошибку давно заметили.



Заключение


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

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

Несколько вещей, которые мы узнали, работая с IDEA:

  1. Расширять IDEA/PhpStorm гораздо проще, чем может показаться на первый взгляд. При разработке вас будет поддерживать одна из передовых сред разработки (IntelliJ IDEA).
  2. Архитектура IDEA построена таким образом, что для добавления нового функционала не требуется ничего менять: просто реализуем класс соответствующего интерфейса — и готово!
  3. В любой непонятной ситуации вам окажут поддержку разработчики JetBrains и сообщество в течение пары дней. На самом деле, я не видел, чтобы где-нибудь ещё разработчики платформы так активно поддерживали сообщество.
  4. Небольшая ложка дёгтя — это документация, которая, к сожалению, покрывает только небольшую часть возможностей платформы. Однако это компенсируется активным сообществом и умением гуглить (очень помогает поиск в гитхабе по коду).
  5. Ещё одна ложка дёгтя (хотя это можно рассматривать и как преимущество) — это очень быстрое развитие платформы. Видно, что разработчики стараются сохранять обратную совместимость до последнего, но всё же обновлять код, заменяя вызовы deprecated-методов и классов, приходится регулярно. Чтобы это не влияло на конечных пользователей, мы всегда собираем код с версией EAP, чтобы при выходе новой версии быть к ней готовыми.


Если вам интересно начать использовать наш плагин в своей работе, присоединяйтесь к нашей команде, мы активно ищем людей.

Ссылки


IntelliJ Platform UI Guidelines
IntelliJ Platform SDK DevGuide
IntelliJ IDEA Open API and Plugin Development (community)
Intellij IDEA Community sources

Пример плагина, упоминавшегося в статье: github.com/badoo/idea-printf-checker-plugin-example

Возможно, вы ещё не слышали, что мы расширяем лондонскую часть команды. До 1 марта можно пройти тест, по результатам которого лучших участников мы пригласим на собеседование в Москве. Успешно пройденное собеседование — оффер в Лондон в тот же день. Билеты до места проведения интервью и релокация — за счёт компании.
Источник: habr.ru