Pull to refresh

php-cs-fixer: Пишем свой фиксер

Reading time 5 min
Views 17K

Качество кода не только в том, как он работает, но и в том как выглядит. То, что единый в рамках кампании code style — это очень важная вещь — в наши дни убеждать уже никого не нужно. Код должен быть не только написан, но и оформлен. В плане оформления PHP кода, утилита php-cs-fixer давно уже стала стандартом. Использовать ее довольно просто, есть куча правил и можно удобно забиндить ее запуск на какую-нибудь комбинацию клавиш в шторме или на pre-commit hook в гите. Все это легко гуглится и подробно разбирается в сотнях статей. А мы сегодня поговорим о другом. Хотя в php-cs-fixer есть большое количество разных фиксеров, но что, если нам понадобится такой, которого там нет? Как написать собственный фиксер?

Фиксер?


Вообще, что такое фиксер? Фиксер, это небольшой класс, который фиксит ваш код, приводит его к какому-то виду. Я не стал выдумывать глупые или сложные кейсы для нового фиксера, и решил взять какой-нибудь вполне реальный. Например, приведение всех ключевых слов в коде к нижнему регистру. За это отвечает фиксер LowercaseKeywordsFixer. Давайте на его примере научимся создавать собственные фиксеры.

Фиксим


Итак, вы уже выполнили
git clone https://github.com/FriendsOfPHP/PHP-CS-Fixer.git
composer install

Наш подопытный фиксер состоит из двух частей:
Сам фиксер:
src/Fixer/Casing/LowercaseKeywordsFixer.php
И тест:
tests/Fixer/Casing/LowercaseKeywordsFixerTest.php
LowercaseKeywordsFixer.php — это файл, который содержит класс фиксера. Каждый фиксер должен наследоваться от абстрактного класса PhpCsFixer\AbstractFixer, а значит содержать методы:
getDefinition();
isCandidate(Tokens $tokens);
applyFix(\SplFileInfo $file, Tokens $tokens);


К этим методам мы еще вернемся. Давайте теперь рассмотрим очень важное для нас понятие: Token.

Token в PHP


Если вы хорошо знакомы с PHP, то понятие токенов для вас не ново. На русском их еще иногда называют “метками”. Токены — это языковые лексемы PHP. Например, если взять такой простенький код:
<?php

foreach ($a as $B) {
   try {
       new $c($a, isset($b));
   } catch (\Exception $e) {
       exit(1);
   }
}


и разбить его на токены, то получим массив из 54 элементов. Вторым элементом будет:

Array
        (
            [0] => 334
            [1] => foreach
            [2] => 3
        )


Где 334 — это идентификатор токена. То есть не этого конкретного токена, а этого типа токенов. Другими словами, все токены, представляющие конструкцию foreach — будут иметь идентификатор 382. Этому идентификатору соответствует константа T_FOREACH. Список всех констант можно посмотреть в документации.

Очень важный момент. Идентификаторы меняются от версии к версии PHP интерпретатора, ваш код никогда не должен зависеть от конкретных цифр, только константы!

Подробнее про токены можно почитать в документации.

Token в php-cs-fixer


В php-cs-fixer есть два класса для работы с токенами:
PhpCsFixer\Tokenizer\Tokens для работы с массивом токенов, и
PhpCsFixer\Tokenizer\Token для работы с одним токеном.
Рассмотрим некоторые полезные методы.

Token:
equals($other, $caseSensitive = true)

Проверяет, что переданный первым параметром токен эквивалентен текущему. Это самый правильный способ проверить, что токены равны.

equalsAny(array $others, $caseSensitive = true);

Проверяет, что один из переданных в первом параметре токенов равен текущему.

getContent();

Получить содержимое токена.

setContent($content);

Задать содержимое токена.

isChanged();

Был ли токен уже модифицирован.

isKeyword();
isNativeConstant();
isMagicConstant();
isWhitespace();

Названия говорят сами за себя.
Подробнее


Tokens:
findBlockEnd($type, $searchIndex, $findEnd = true);

Найти конец блока типа $type (фигурные, квадратные или круглые скобки), начиная от токена с индексом $searchIndex. Если третьим параметром передать true — то метод будет искать начало блока, а не конец.

findGivenKind($possibleKind, $start = 0, $end = null);

Найти токены заданного типа (типов, если передать массив) начиная с токена под индексом $start и до токена под индексом $end.

generateCode();

Сгенерировать PHP код из набора токенов.

generatePartialCode($start, $end);

Сгенерировать PHP код из набора токенов между $start и $end

getNextTokenOfKind($index, array $tokens = array(), $caseSensitive = true);

Найти следующий токен определенного типа

getNextMeaningfulToken($index);
getPrevMeaningfulToken($index);

Найти следующий/предыдущий токен, содержащий что-то, кроме пробелов и комментариев.

insertAt($index, $items);

Добавить в коллекцию новый токен, после $index

overrideAt($index, $token);

Заменить токен с индексом $index на переданный вторым параметром.
Подробнее


Пишем фиксер


Теперь к самому фиксеру.
Напомню, что мы пишем фиксер, который приводит все ключевые слова PHP к нижнему регистру. Класс фиксера будет находиться в файле
src/Fixer/Casing/LowercaseKeywordsFixer.php
Для начала нам нужно определить, попадает ли код под наш кейс. В нашем случае нам надо обработать любой код, который содержит ключевые слова php. Определим метод isCandidate.

public function isCandidate(Tokens $tokens)
{
   return $tokens->isAnyTokenKindsFound(Token::getKeywords());
}

Теперь нам нужно описать наш фиксер. Для этого определим метод:
public function getDefinition()
   {
       return new FixerDefinition(
           'PHP keywords MUST be in lower case.',
           array(
               new CodeSample(
'<?php
   FOREACH($a AS $B) {
       TRY {
           NEW $C($a, ISSET($B));
           WHILE($B) {
               INCLUDE "test.php";
           }
       } CATCH(\Exception $e) {
           EXIT(1);
       }
   }
'
               ),
           )
       );
   }


Этот метод возвращает объект FixerDefinition, конструктор которого принимает два параметра: короткое описание фиксера (оно будет в документации в файле README.rst) и небольшой пример кода для исправления (он нигде отображаться не будет, но участвует в тестах).

Также мы можем реализовать метод
public function getPriority()
{
   return 0;
}

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

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

protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
   foreach ($tokens as $token) {
       if ($token->isKeyword()) {
           $token->setContent(strtolower($token->getContent()));
       }
   }
}


В итоге должен получиться примерно такой файл.

Что дальше


У нас есть работающий фиксер. Это здорово. Осталось совсем чуть-чуть. Давайте напишем для него тест. Наш тест будет находиться в файле
tests/Fixer/Casing/LowercaseKeywordsFixerTest.php
Это обычный PHPUnit тест, разве что у него есть свой метод
doTest($expected, $input = null, \SplFileInfo $file = null)

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

/**
* @param string      $expected
* @param null|string $input
*
* @dataProvider provideExamples
*/
public function testFix($expected, $input = null)
{
   $this->doTest($expected, $input);
}

Напишем провайдер данных:

public function provideExamples()
{
   return array(
       array('<?php $x = (1 and 2);', '<?php $x = (1 AND 2);'),
       array('<?php foreach(array(1, 2, 3) as $val) {}', '<?php FOREACH(array(1, 2, 3) AS $val) {}'),
       array('<?php echo "GOOD AS NEW";'),
       array('<?php echo X::class ?>', '<?php echo X::ClASs ?>'),
   );
}

В итоге получаем такой код.

Тест работает, и если запустить только его, то все пройдет успешно. А вот общий тест сфейлится, т.к. данных о нашем фиксере нет в документации. Документация в php-cs-fixer авто-генерируемая, значит, достаточно запустить:
php php-cs-fixer readme > README.rst

И информация о нашем фиксере добавится в документацию.
Теперь нужно проверить оба наших файла на предмет соответствия код стайлу:
php ./php-cs-fixer fix

Ну и в конце концов запустить общий тест:
phpunit ./tests

Если все прошло успешно, то ваш собственный фиксер готов. Далее можно сделать пулл реквест и через какое-то время ваше творение появится в php-cs-fixer.
Tags:
Hubs:
+27
Comments 0
Comments Leave a comment

Articles