Работа в PHP с Tokenizer

Для справки:
Tokenizer (лексер) предоставляет интерфейс для анализа кода. Таким образом, можно писать утилиты без необходимости работы с языковой спецификацией.
Tokenizer, начиная с версии php >= 4.3 включен в сборку php по-умолчанию.


Какие задачи можно решать с помощью tokenizr'а?
Да самые разные, связанные с анализом и модификацией кода.

Удаление комментариев из кода


Самый простой пример приведен на php.net — по удалению комментариев:

<?php
function strip_comments($fileName)
{
    $source = file_get_contents($fileName);
    // получаем все метки файла
    $tokens = token_get_all($source);
    $result = '';

    foreach ($tokens as $token) {
        if (!is_array($token)) {
            // простая 1-буквенная лексема
            $result .= $token;
        } else {
            // токен-массив
            list($id, $value) = $token;

            switch ($id) {
                case T_COMMENT:
                case T_DOC_COMMENT:
                    // комментарии пропускаем
                    break;

                default:
                    // все остальное -> оставляем "как есть"
                    $result .= $value;
                    break;
            }
        }
    }
    return $result;
}
?>

Как видно из кода — мы получаем массив токенов и, в зависимости от их типа, оставляем или пропускаем.
Так можно решить задачки и поинтереснее — например на основе php-файлов сгенерировать карту классов проекта для автозагрузки.

Получение списка классов из файла


Для получения списка классов из файла я написал вот такую функцию:

<?php
function getClasses($fileName)
{
    $result = array();
    $content = file_get_contents($fileName);
    $tokens = token_get_all($content);
    $waitingClassName = false;
    $waitingNamespace = false;
    $waitingNamespaceSeparator = false;
    $namespace = array();
    for ($i = 0, $c = count($tokens); $i < $c; $i++) {
        if (is_array($tokens[$i])) {
            list($id, $value) = $tokens[$i];
            switch ($id) {
                case T_NAMESPACE:
                    $waitingNamespace = true;
                    $waitingNamespaceSeparator = false;
                    $namespace = array();
                    break;
                case T_CLASS:
                case T_INTERFACE:
                    $waitingClassName = true;
                    break;
                case T_STRING:
                    if ($waitingNamespace) {
                        $namespace[] = $value;
                        $waitingNamespace = false;
                        $waitingNamespaceSeparator = true;
                    } elseif ($waitingClassName) {
                        if (!empty($namespace)) {
                            $value = sprintf('%s\\%s', implode('\\', $namespace), $value);
                        }
                        $result[] = $value;
                        $waitingClassName = false;
                    }
                    break;
                case T_NS_SEPARATOR:
                    if ($waitingNamespaceSeparator && !$waitingNamespace && !empty($namespace)) {
                        $waitingNamespace = true;
                        $waitingNamespaceSeparator = false;
                    }
                    break;
            }
        } else {
            if (($waitingNamespace || $waitingNamespaceSeparator) && ($tokens[$i] == '{' || $tokens[$i] == ';')) {
                $waitingNamespace = false;
                $waitingNamespaceSeparator = false;
            }
        }
    }
    return $result;
}
?>


А потом подумал и написал небольшую утилиту, которая генерирует автозагрузчик на основании файлов проекта.
Она анализирует в указанной папке все файлы с расширением "*.php" и строит карту классов (с учетом неймспейсов, конечно), на основании которой потом генерируется автозагрузчик.
Найти ее можно на github.com

Отключение и переопределение стандартных функций


На днях вспомнил, как когда-то ковырялся с расширением runkit. Из его возможностей меня особенно заинтересовало переопределение стандартных функций и песочница, в которой можно было бы запретить использование тех или иных функций.
И сейчас я подумал, а можно ли реализовать подобный функционал, без использования этого расширения. Оказалось, что tokenizer вполне может помочь в этом деле.
Так родилась библиотечка Runtime, с помощью которой, можно во время выполнения скрипта запретить использование любых стандартных функций, или переопределить их.
Приведу примеры работы:

<?php
use Dm\Runtime;

$code = <<<CODE
<?php
echo str_replace( 0, 1, 100 );
?>
CODE;

// Вывалит Exception, с в котором сообщается, что использование str_replace запрещено
Runtime::code($code)
    ->disableFunction('str_replace')
    ->execute();
?>


<?php
use Dm\Runtime;

$code = <<<CODE
<?php
echo str_replace( 0, 1, 100 );
?>
CODE;

// Выведет 000, вместо 111
Runtime::code($code)
    ->overrideFunction('str_replace', function ($search, $replace, $subject) {
        //  меняем 1ый и 2й аргументы местами
        echo str_replace($replace, $search, $subject);
    })
    ->execute();
?>


Как использовать эти возможности — дело личное. Но использовать это нужно аккуратно.
Я провел небольшое исследование и остался доволен результатом.
Касательно runtime — сложно сказать, где его можно применить, а где нет. Но сама библиотека наглядно демонстрирует работу tokenizer'а и его возможностей.

Ссылки

  1. Tokenizer
  2. Runkit
  3. Генерация автозагрузчика на github.com
  4. Библиотечка Runtime на github.com
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 29
  • +8
    не показывайте это разработчикам MODx (они скрипты в БД держат....)
    • 0
      Боюсь они это сами могут увидеть.
      • 0
        Почему не показывать? ;-)
      • +1
        Хотел было предложить написать расширение для PHPUnit, чтобы можно было мокать нативные функции в тестах, но оказалось, что такое уже есть.
        • 0
          Ага. Только, если я правильно понял, там перекрытие функций осуществляется за счет namespace'ов, что проще и, думаю, даже правильнее в том контексте.
        • 0
          www.php.net/manual/ru/features.commandline.options.php
          -w Отображает исходный текст без комментариев и пробелов

          Composer — если указать папку для файлов:
          autoload": {
          «classmap»: [«lib»],

          — сгенерит autoload_classmap.php файл со списком всех файлов, обработает и папки и подпапки.
          • 0
            Все таки сравнивать запуск из консоли с обработкой tokenizer'ом не совсем корректно. Безусловно такого же результата можно добиться и другими способами. Я привел простейший пример обработки.

            Что касается composer — конечно, я знал, что у него есть такая возможность. Но опять же, мой генератор автозагрузки родился в результате экспериментов с tokenizer'ом. И он так же генерирует список всех файлов, из подпапок и подпапок подпапок.
            Разница лишь в том, что тут одна функция, которая заправляет всем анализом и по которой можно понять принцип работы tokenizer'а, а composer — это пакетный менеджер, в котором ни одна тысяча строчек кода.
            • +3
              Специально открыл исходники composer по части генерации автозагрузчика — и увидел следующее:
              // ... тут пара регулярок, затем вот это
              preg_match_all('{
                          (?:
                               \b(?<![\$:>])(?P<type>class|interface'.$traits.') \s+ (?P<name>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)
                             | \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\s*\\\\\s*[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)? \s*[\{;]
                          )
                      }ix', $contents, $matches);
              // .. еще пара регулярок
              


              Я ничего не имею против регулярных выражений, но разобраться тут сложно, да и подход совершенно другой. Прелесть tokenizer'а в том, что он обрабатывает именно языковые конструкции, а не все подряд. Т.о. я уверен, что моя функция обработает только php-код.
              • 0
                для такой простой задачи как реализовано — не суть. главное что работает как надо.

                а использование tokenizer имеет смысл при реализации например хитрой системы макросов для php или
                конвертера кода под новые версии php в старые версии и наоборот.

          • +1
            А еще можно автоматически парсить проект, вытаскивать оттуда локализационные термины и автоматически генерировать/обновлять языковой файл. Я так давно уже делаю.
            • 0
              И это весьма достойное применение tokenizer'у.
            • +1
              А, например, я с помощью токенайзера узнаю в каком методе в моём проекте на symfony 1.4 произошла ошибка. Беру из эксепшна file и line, строю дерево и нахожу ближайший сверху токен функции.
              • 0
                Мне казалось, я что-то подобное видел в yii. Там при ошибке/эксепшене отображалась функция, в которой это событие произошло.
                Хотелось бы взглянуть на реализацию, в yii я побоялся лезть. Применение очень интересное.
                • 0
                  Это дерево вызовов (история), не совсем то. Называется debug_backtrace(). Используется, например, при реализации Singleton в PHP5.2.
                  • 0
                    Что такое debug_backtrace() я знаю, просто в ассоциациях всплыла именно эта фишка, красиво у них она реализована. Как она устроена, я не смотрел, но предположил, что именно так ее можно реализовать. В любом случае, спасибо за поянение.
              • +1
                Еще одна возможность, которую обеспечивает токинайзер — написание простого обфускатора кода.
                Так же, есть мысль использовать его для анализа качества сторонних решений для коробочного продукта. Например, можно выявить, что разработчик из модели пытается обратиться к контроллеру или из контроллера напрямую взаимодействовать с бд (неймспейсы очень помогают в этом).
                • 0
                  С обусфактором вопрос спорный, тут наверное все же лучше использовать какое-то готовое решение т.к. система может получиться не шуточной, но возможность такая есть конечно.
                  А вот с анализом коробочного продукта — идея хорошая, я приму на заметку, спасибо!
                  • 0
                    У меня обфускатор получился не более 80-100 строк кода. он, конечно, не идеален, но решает маркетинговые задачи не хуже готовых коробочных. учитывая, что при желании любой обфусцированный код можно деобфусцировать, а для сторонних решений есть уже готовые деобфускаторы, то простой обфускатор, использующий токинайзер более чем не плох:)
                • 0
                  Так родилась библиотечка Runtime, с помощью которой, можно во время выполнения скрипта запретить использование любых стандартных функций, или переопределить их.
                  

                  Если я сделаю eval(«func();») тоже будет работать запрет на вызов функции?
                  • 0
                    Я думал над этой возможностью, но совсем сильно заморачиваться не хотелось. Пришел к выводу, что проще запретить eval() и create_function() в такой ситуации.
                    • +1
                      Есть тысячи способов вызвать функцию без create_function и eval. Для этого нужно использовать любую функцию, которая используется callback или вызвать её через call_user_func, или через переменную: $funcname().
                      • 0
                        Ага, такие варианты тоже есть. Их достаточно сложно отловить, поэтому эти ситуации я не стал обрабатывать.
                        В таких ситуациях лучше пользоваться опцией disable_functions или расширением runkit, чтобы запретить выполнение на уровне интерпретации.
                        • 0
                          А вообще, это интересная задача, возьму на заметку и попробую реализовать и такие проверки.
                    • +1
                      все бы было хорошо, но насколько я знаю token_get_all наглухо затыкается при встрече чего-то не слишком валидного, или смешанного с НЕ-php. Поэтому например Doctrine до сих пор использует парсер аннотаций на регулярках, хотя было бы круто если бы они уже были по RFC и tokenizer умел их обрабатывать как-следует. А вообще я целиком ЗА дальнейшего развития встроенного парсера.
                      • 0
                        > наглухо затыкается при встрече чего-то не слишком валидного
                        Эм… код либо валидный, либо нет. Если парсер работает не так как хочется на невалидном коде, то я даже не знаю к кому претензии предъявлять.

                        > или смешанного с НЕ-php.
                        О чём именно речь?

                        > Поэтому например Doctrine до сих пор использует парсер аннотаций на регулярках…
                        «Поэтому»? Для языка эти аннотации это всего лишь комментарии.

                        > А вообще я целиком ЗА дальнейшего развития встроенного парсера.
                        В какую сторону?
                        • 0
                          «Поэтому»? Для языка эти аннотации это всего лишь комментарии.
                          Может расскажешь это wiki.php.net/rfc/annotations?
                          В какую сторону?
                          См. выше. в сторону развития.
                        • 0
                          Как сказал товарищ sectus, — token_get_all может затыкаться из-за невалидного кода.
                          Я проверял работоспособность на смешанном коде, типа
                          - Привет, друзья!
                          <?php
                          echo "<br/> - Как дела?";
                          ?>
                          <br /> - Да все в порядке!
                          


                          При парсинге кода я сперва разделяю код на блоки php и не-php. То, что не-php — оборачиваю в nowdoc/heredoc, а потом склеиваю. Таким образом получается монолитный php-код, который можно нормально парсить. Производительность падает, конечно, из-за такого анализа, но пока идей лучше мне не пришло.
                          Парсер аннотаций и регулярки конечно же могут справиться с подобной задачей, вот только это не анализ кода получается, а анализ текста. А текст, сами понимаете, может быть совсем не таким, как ожидалось.

                          upd: А под встроенным парсером вы что понимаете? tokenizer или либу?
                        • 0
                          tokenizer-либу! Я и говорил выше что приходится подыскивать больному костылики… хотя по-хорошему больной должен не только излечиться, но и прозреть научившись сам определять что за набор букаф ему подсунули иначе можно с таким же успехом регулярками парсить текст а не tokenizer'ом.
                          Или еще худший костыль встречается — чтобы tokenizer понял код, давайте его сначала делать валидным! Вот вас в ту степь понесло, хоть и не так глубоко как некоторых.
                          • 0
                            > tokenizer-либу
                            Она использует лексичекий парсер самого языка. Поэтому, чтобы токенайзер смог разбирать аннотации, аннотации должны стать частью языка.

                            > Может расскажешь это wiki.php.net/rfc/annotations?
                            А чего говорить то? Это предложение(не единственное) о добавление парсинга аннотаций… в отражениях(т.е. никак не влияет на работу токенайзера). Только уже существуют библиотеки по парсингу аннотаций. Не думаю, что реализация этих библиотек как-то поможет развитию самого языка.

                            > Или еще худший костыль встречается — чтобы tokenizer понял код, давайте его сначала делать валидным! Вот вас в ту степь понесло, хоть и не так глубоко как некоторых.
                            У вас «костный язык» — не ясно что именно Вы хотели сказать. Покажите на примерах то, что вы хотите от него и с чем он не справляется.

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