Статический анализ PHP-кода с помощью HipHop

    Неожиданно не нашёл информации на русском языке о такой замечательной возможности HipHop, как статический анализ кода для PHP, а потому встречайте обзор, на идею которого меня натолкнула презентация Расмуса на DevConf.

    А как это вообще?

    Статический анализ кода — вещь весьма полезная, ведь иначе ошибку мы не увидим, пока функция, её содержащая, не будет вызвана. Как же это делает HipHop? Он транслирует PHP в C++!

    Таким образом мы получаем возможность статически проанализировать C++ код, что, в общем, давно никого не удивляет, а потом применить полученную информацию к PHP (естественно автоматически).

    Итак, начнём.

    Окружение

    Для тестов я поднял новый сервер Centos 5.8 x64
    # uname -a
    Linux TEST 2.6.18-308.8.1.el5xen #1 SMP Tue May 29 15:38:25 EDT 2012 x86_64 x86_64 x86_64 GNU/Linux
    


    Установил на нём HipHop
    rpm -ivh http://epel.osuosl.org/5/x86_64/epel-release-5-4.noarch.rpm
    rpm -ivh http://dl.iuscommunity.org/pub/ius/stable/Redhat/5/x86_64/ius-release-1.0-10.ius.el5.noarch.rpm
    rpm -ivh http://pkg.tag1consulting.com/hphp/x86_64/hphp-release-1.0-2.el5.noarch.rpm
    yum install hiphop-php -y
    


    Добавил переменную окружения (почему-то сам RPM этого не сделал)
    export HPHP_HOME=/usr/lib64/hphp


    И создал папку для отчётов
    mkdir /var/reports
    export SRC_DIR=/var/www/
    export OUTPUT_DIR=/var/reports


    Теперь окружение готово и можно приступать к экспериментам.

    Подготовка к анализу


    Сначала нужно составить список файлов, которые мы будем анализировать.
    Я анализировал один тестовый файлик, но если вы хотите проверить сразу весь свой проект, то можете сделать так:
    find $SRC_DIR -name '*.php' > $OUTPUT_DIR/files.txt


    После того, как список файлов составлен, надо скормить его HipHop'у со следующими параметрами:
    --target=analyze — зачем мы вызвали HipHop?
    --input-list=$OUTPUT_DIR/files.txt — список файлов для проверки
    --input-dir=$SRC_DIR — где лежат файлы, которые нужно проверить?
    --output-dir=$OUTPUT_DIR — куда сохранить результаты проверки?
    --gen-stats=1 — создать файл со статистической информацией об ошибках
    --log=3 — уровень лога, который выводится в консоль (3 — оптимально)
    --force=1 — игнорировать найденные ошибки и продолжать анализ
    --parse-on-demand=0 — не трогать файлы, которых нет в списке

    Анализ

    Поехали!
    $HPHP_HOME/src/hphp/hphp --input-list=$OUTPUT_DIR/files.txt --input-dir=$SRC_DIR --output-dir=$OUTPUT_DIR --gen-stats=1 --log=3 --force=1 --parse-on-demand=0 --target=analyze


    Кстати, если в процессе парсинга у вас вылетают ошибки вроде "Unable to parse file: /var/www/user.php\n (Line: 14, Char: 23):
    Это значит, что в файле синтаксическая ошибка и он вообще не работает, так что обратите на него внимание!

    Результат анализа


    Когда hphp завершил свою работу (а это происходит удивительно быстро!), в папке /var/reports/ появляются два файла:
    CodeError.js и Stats.js

    Содержимое последнего расшифровывается примерно так:
    Array
    (
        [FileCount] => 125
        [LineCount] => 11784
        [CharCount] => 433255
        [FunctionCount] => 350
        [ClassCount] => 17
        [TotalTime] => 0
        [AvgCharPerLine] => 36
        [AvgLinePerFunc] => 33
        [SymbolTypes] => Array
            (
                [Array] => 64
                [Boolean] => 43
                [Double] => 30
                [Int32] => 68
                [Int64] => 2208
                [Numeric] => 23
                [Object] => 11
                [Object - Specific] => 58
                [Primitive] => 31
                [Sequence] => 2
                [String] => 221
                [Variant] => 1458
                [_all] => 4217
                [_strong] => 2703
                [_weak] => 1514
            )
    
        [VariableTableFunctions] => Array
            (
            )
    
    )

    На мой взгляд всё понятно и пояснять тут нечего.

    А вот CodeError.js надо рассмотреть поподробнее.
    Он тоже в формате JSON и содержит информацию обо всех ошибках, что нашёл HipHop.
    А ищет он следующие виды неисправностей:

    • Отсутствующие файлы для require \ include
    • Вызовы неопределённых функций / методов
    • Обращение к неопределённым переменным, классам, константам
    • Переопределение констант
    • Одинаковые имена аргументов в определении функции
    • Обязательные аргументы после необязательных в определении функции
    • Недостаточность \ избыточность аргументов при вызове функции
    • Код, который не выполнится ни при каких условиях
    • Методы и функции, возвращающие void
    • Использование $this в статических методах
    • И много другое
      BadPHPIncludeFile, PHPIncludeFileNotFound, UnknownClass, UnknownBaseClass, UnknownFunction, UseEvaluation, UseUndeclaredVariable, UseUndeclaredGlobalVariable, UseUndeclaredConstant, UnknownObjectMethod, InvalidMagicMethod, BadConstructorCall, DeclaredVariableTwice, DeclaredConstantTwice, BadDefine, RequiredAfterOptionalParam, RedundantParameter, TooFewArgument, TooManyArgument, BadArgumentType, StatementHasNoEffect, UseVoidReturn, MissingObjectContext, MoreThanOneDefault, InvalidArrayElement, InvalidDerivation, InvalidOverride, ReassignThis, MissingAbstractMethodImpl, BadPassByReference, ConditionalClassLoading, GotoUndefLabel, GotoInvalidBlock, AbstractProperty, UnknownTrait, MethodInMultipleTraits, UnknownTraitMethod, InvalidAccessModifier, CyclicDependentTraits, InvalidTraitStatement, RedeclaredTrait, InvalidInstantiation


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

    Для того, чтобы посмотреть все ошибки в удобочитаемом формате можно использовать скрипт от Patrick Allaert либо написать что-нибудь своё красивое и с плюшками =)

    Пример

    Есть вот такой ужасный файл
    <?php
    ini_set('display_errors', 0);
    
    require("$a.php");
    
    define('CONSTANT', 1);
    
    function myfunc($arg1 = 'optional', $arg2) {
        echo $missing_var;
    }
    
    define('CONSTANT', 1);


    Запустим на него тест, а потом попробуем посмотреть ошибки
    ./CodeErrorFormatter.php /var/report/CodeError.js


    Oops, у меня PHP выдал ошибку getopt, но я просто вписал путь к файлу ручками и получил вот такой вывод:
    CodeErrorFormatter 1.1.0 by Patrick Allaert.

    UseUndeclaredVariable
    ================================================================================

    /var/www/index.php
    9:9 -> 9:21 details: $missing_var
    4:9 -> 4:11 details: $a
    DeclaredConstantTwice
    ================================================================================

    /var/www/index.php
    12:0 -> 12:21 details: define('CONSTANT', 1)
    RequiredAfterOptionalParam
    ================================================================================

    /var/www/index.php
    8:16 -> 8:41 details: $arg2


    Как видите, здесь указаны для каждой ошибки тип, адрес файла, название непонравившейся фукции \ переменной, её строка и позиция в файле. Что с ними делать дальше — уже сами думайте (либо автоматический багрепорт, либо сами исправляйте, либо ещё что).

    Заключение

    В общем тема статического анализа для PHP не нова, однако русскоязычных статей на эту тему немного и ни одна из них не затрагивает HipHop.

    Призываю всех просто попробовать ею воспользоваться. Разумеется не стоит думать, что это «серебряная пуля», но анализ показывает те вещи, которые очень трудно выловить именно человеку.

    Надеюсь этот обзор принесёт кому-то пользу, а может кто-то ещё и напишет визуалку, чтобы красиво тестировать код из браузера?

    ===================

    Проект Facebook HipHop
    CodeError.js CodeFormatter
    Презентация Расмуса Лердорфа на DevConf 2012 talks.php.net/show/devconf/ — обязательно к просмотру!
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 62

      +8
      А насколько оно эффективнее чем испектор кода в phpStorm?
        +1
        phpStorm вы с CI серверу не привяжите.
          0
          Пардон за серость, но CI — это что? Дизамбиг в вики длинноватый.
            0
            Continuous Integration
              0
              Ошибка, обнаруженная ХипХопом — это гарантированно ошибка или просто предупреждение о возможной ошибке? Если первое, то верится с большим трудом (привет eval и иже). Если второе, то теряется смысл, потому что при ложном срабатывании будет или сломанный билд (при суровом подходе), или результат, набитый предупреждениями, на которые всем пофиг (если не сразу пофиг, то рано или поздно будет).
                0
                Надо пробовать, автор же писал, что пока ещё мало кто тестировал.
                  0
                  Насколько я помню, к ограничением хипхопа относится запрет на использование евала.

                  По поводу обнаруженных ошибок — и то и другое. Отсутствующий инклюд — это явная ошибка. Лишний параметр при вызове функции — возможная.

                  Сломанного билда не будет, поскольку тестирование делается перед коммитом.

                  Варнингов на [плохом] коде будет много, да. Расмус так и сказал — «кто хочет затроллить багтрекер любого опенсорс проекта, может натравливать анализатор хипхопа и писать тикеты десятками. „
                  Собственно, чтобы не было много предупреждений — нужно писать чистый код. И начинать тестировать его сразу, а не после того, как написано 100500 строк.
                  Скажем, во время презентации Расмус нашёл место в Вордпрессе, где в случае ошибки при fopen()… делался fclose()! И это очевидный говнокод.

                  Собственно, многие забывают, что куча варнингов “на которые всем пофиг» — это не проблема анализатора, это проблема кода.

                    0
                    Никогда не понимал подход игнорирования варнингов.
                    Да и вообще, сам факт написания такого кода.
                    У меня все нотисы/варнинги/ерроры всегда включены (отключаю только на финальной публикации) — и во-первых они вылазят крайне редко по ходу работы, во-вторых я их никогда не пропускаю.

                    Как же нужно наплевательски относиться к своему коду, что бы пропускать предупреджения?
                      0
                      У меня при разработке аналогично:
                      error_reporting => E_ALL (до PHP 5.4 было Е_ALL | E_STRICT)
                      xdebug.scream = 1 (отключает подавление ошибок при помощи @)

                      Да только люди разные: кто-то любит свою работу, кто-то лепит как попало лишь бы работало и от него отстали.
                      Многое еще зависит от того как на работе поставлен весь процесс разработки.
                        0
                        ну собаку я пользую в некоторых случаях для проверки, сериализована ли строка ( if ($tmp=@unserialize($str)) $str=$tmp; )
                        к сожалению, другого способа нет, а иногда такие проверки нужны
                          0
                          На продакшене display_errors => Off (только в лог) и нет xdebug :)

                          Что касается вашего примера, есть как минимум один случай, когда он не сработает.
                            0
                            Какой?
                              0
                              Если сериализованая строка будет такой «b:0;»
                                0
                                Согласен. Но в тех случаях, в которых я использую такую проверку, такого не бывает)
                                  0
                                  Есть способ проверки и без собаки, но он завернутый (несколько проверок и регэкспы).
                                  Если будет интересно, напишите в личку, завтра вышлю.
                                    0
                                    у нас вот такое используют:
                                    function is_serialized($data)
                                    {
                                    // if it isn't a string, it isn't serialized
                                    if (!is_string($data)) return false;
                                    $data = trim($data);
                                    if ('N;' == $data) return true;
                                    if (!preg_match('/^([adObis]):/', $data, $badions)) return false;
                                    switch ($badions[1]) {
                                    case 'a':
                                    case 'O':
                                    case 's':
                                    if (preg_match("/^{$badions[1]}:[0-9]+:.*[;}]\$/s", $data)) return true;
                                    break;
                                    case 'b':
                                    case 'i':
                                    case 'd':
                                    if (preg_match("/^{$badions[1]}:[0-9.E-]+;\$/", $data)) return true;
                                    break;
                                    }
                                    return false;
                                    }
                            0
                            Вы допускаете очень распространённую ошибку, о которой я говорил на конференции:
                            Ставя собаку, РНР программист полагает, что ошибка бывает только одного типа.
                            Это неверно, и на этом заблуждении было потрачено впустую немало человеко-часов.

                            Для таких исключительных случаев лучше использовать исключения —
                            Ставить трай и отлавливать ошибку в кетче.
                            Ошибку ПРОВЕРЯТЬ, та ли это, которую мы ждём.
                            Если та — продолжаем выполнение программы.
                            Если нет — выбрасываем эту же ошибку снова, через trigger_error

                            Для этого надо переопределять error_handler, разумеется, но это и так везде сделано, мне кажется.

                            Разумеется, это не слишком красиво, и такой кривизны лучше избегать, меняя логику программы.
                            Скажем, сама проверка «сериализована ли строка» говорит о непродуманности логики программы. Хорошая программа всегда знает, какого рода данные ожидаются на вход.

                            Но в любом случае, ставя собаку всегда следует помнить, что ошибки бывают РАЗНЫЕ, а не только те, про которые мы знаем и которые ждём ;)
                              0
                              Некоторые программы имеют динамическую структуру данных. Ну, бывает такое)
                              Т.е. данные могут добавляться, меняться, типы данных могут быть разные, методы хранения и т.п.

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

                              Что касается try/catch — ну да, соглашусь, правильнее это делать через него. Надо себя заставлять))))
                          0
                          1. Вы пропустили контекст.
                          Речь не о предупреждениях РНР, а об «ошибках, обнаруженных ХипХопом».
                          Я в комментарии ниже приводил слова Расмуса о том, что статический анализатор выполняет в 10 раз больше работы, чем обычный интерпретатор, и ошибки, которые он выдаёт, не отлавливаются обычным PHP. Это получается что-то вроде super-strict mode.
                          К примеру, Хипхоп отлавливает код, который никогда не выполняется, или передачу функции большего количества параметров, чем было объявлено. PHP на это не обращает внимания, а хипхоп — ругается.

                          2. Отключая ошибки «на финальной публикации» вы допускаете большую ошибку. На боевом сервере они нужны не меньше, чем на тестовом. Отключен должен быть только вывод на экран, разумеется. А сами «нотисы/варнинги/ерроры» должны быть включены так же как на тестовом — по максимуму, и писаться в лог.
                            0
                            1. Я ответил на вторую часть вашего сообщения)

                            2. Конечно же я имел ввиду вывод их на экран, а не отключение вовсе
                  0
                  Почему? Он плагины поддерживает, а плагином можно необходимую прослойку сделать.
                    0
                    Вы представляете себе процесс непрерывной интеграции?
                      0
                      Похоже, что не представляет.
                        0
                        Да. В чем видите проблему?
                          0
                          Подскажите как запустить идею без иксов?
                            0
                            <irony>А давайте еще и без оперативки попробуем?</irony>

                            С иксами.
                              0
                              Зачем на сервере иксы?
                                –3
                                Чтобы запустить phpStorm.
                                Чувствую себя КО.

                                Есть и другие применения, например для работы с документами офисных форматов, но тут есть альтернативы.
                                  +1
                                  Стестняюсь спросить, а на production сервера тоже нужно иксы ставить?
                                    +2
                                    Да, «например для работы с документами офисных форматов» :)
                                      +3
                                      Угу, а для работы с документами графических форматов придётся ставить GIMP.
                                    +1
                                    Вы серьезно предлагаете вместо выполнения нескольких консольных команд поднимать на сервере иксы, писать плагин для идеи, как-то интегрировать это все с CI, и в итоге запускать для каждого билда «монстра» в виде идеи?

                                    Если да, то вопрос один: ЗАЧЕМ?
                                      0
                                      Кроме академического интереса.
                                        0
                                        При условии отсутствия CLI-альтернатив способ имеет право на жизнь.
                                          0
                                          CLI-альтернатива уже предложена.
                                            0
                                            Да и не альтернатива это: статический анализ PHP вообще не возможен, только динамический.
                                              0
                                              * нативного PHP, без трансляции в статические языки.
                                    0
                                    А зачем мне (да и вообще любому здравомыслящему админу) иксы на сервер CI?
                                    Только ради phpStorma? o_O
                                  0
                                  Схема работы:

                                  1. Коммиты в VCS.
                                  2. На сервере стартовал билд.
                                  3. Запустился Code Analyzer.
                                  4. В случае найденных ошибок в коде билд упал, письма отправились, каждый девелопер при просмотре dashboard проекта видит информацию о ошибках.

                                  Какая схема будет с использованием phpStorm?
                                    0
                                    Та же самая.
                                    phpStorm будет работать на шаге 3.
                                    Я не вижу принципиальной проблемы с автоматизацией при помощи phpStorm.
                                      +1
                                      Для меня это сродни забиванию гвоздей микроскопом.
                          +9
                          Анализ такого уровня любая IDE поддерживает. Без Хип Хопа.
                          А чем ещё оно может быть интересно?
                            +2
                            Для CI.
                              0
                              Тем, что не все используют IDE.
                              +6
                              Вам следовало сказать, что это имеет смысл только вы транслируете код с помощью HipHop в C++.

                              Иначе смотрите PHP Mess Detector, PHP_CodeSniffer, PHP Depend
                                –1
                                Вам следовало сказать, что это имеет смысл только вы транслируете код с помощью HipHop в C++.
                                A как вы себе по другому представляете статический анализ?

                                PHP Mess Detector, PHP_CodeSniffer, PHP Depend — это инструменты из другой оперы.
                                  0
                                  К тому же PHP_CodeSniffer — проверка форматирования, а не анализ как таковой.
                                    +2
                                    Совершенно верно!

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

                                    То же относится и к предыдущим комментариям. Все приведённые инструменты прекрасны, но они о другом. Они смотрят просто на код и проверяют его на соответствие набору правил. HipHop делает из него C++ программу и смотрит, работает ли она так, как мы привыкли это видеть при компиляции неинтерпретируемого софта.
                                      +1
                                      Строго говоря, компиляции там не происходит.
                                      Расмус это подчеркнул — «на компиляцию ушло бы минут 20 на моём ноутбуке» — сказал он — «но статический анализ происходит так быстро, что его можно вешать на коммит».

                                      Но смыслу сказанного вами это не противоречит: опять же, со слов Расмуса, «статический анализатор должен сделать в 10 раз больше вещей, чем обычный парсер РНР». Задача анализатора — отследить все возможные косяки до компиляции.
                                      С другой стороны, ничто не мешает ту же самую функциональность реализовать силами IDE — ведь, теоретически, ide может отследить потерянный инклюд, лишние аргументы функции и многое другое. Вопрос — насколько полно и точно они это делают.
                                  +11
                                  мне из Hip-Hop — 2pac нравится
                                  • UFO just landed and posted this here
                                    +1
                                    Тоже понравилась эта часть доклада Расмуса!
                                    Спасибо за более развернутую информацию…
                                      0
                                      image

                                      А теперь этот же момент для ностальгии по Devconf ;)
                                        0
                                        Пардон, из статьи не совсем понятно, в каком виде он выводит результаты. Файлы называются *.js и вы называете этот формат «json», однако в примере явно вывод, аналогичный var_dump().
                                          0
                                          Прошу прощения, я не совсем точно написал «расшифровывается». Расшифровывется JSON в указанный в статье var_dump. Просто содержимое однострочного JSON публиковать в статье некрасиво.
                                          0
                                          > Он транслирует PHP в C++!

                                          Значит, для C++ есть какой-то открытый/бесплатный статический анализатор. Какой именно тут используется?

                                          Пару лет назад озаботился поиском такового, пригодного к использованию не нашел.
                                            0
                                            Тот, о котором идет речь здесь, анализирует PHP. До трасляции в С++.
                                            0
                                            Интересный подход! А есть ли пример С++ кода который получается после выполенения хипхопа на каком-нибудь php файле? Очень любопытно глянуть что там получается…

                                            У перла есть тоже что-то подобное в стандартной поставке, но оно дико глючит, для средних проектов генерит С файлы размером метров по 10, от которых gcc встает раком на полчаса и более.
                                              0
                                              Огромное спасибо за статью.
                                              Анализатор запускается, создает файлы со статистикой и ошибками.

                                              Тем не менее я столкнулся с проблемой того, что файл CodeError.js всегда содержит 0 ошибок, какой бы код я не анализировал (даже с нарочно внесенными ошибками).

                                              Выглядит он всегда вот так:
                                              CodeError.js:
                                              [0,{}
                                              ]
                                              


                                              Сталкивались ли вы с такой проблемой?
                                              Возможно в статье вы не упомянули о каких-то дополнительных настройках, управляющих записью ошибок в файл CodeError.js?
                                                0
                                                А что содержит Stats.js?

                                                Может у вас не сканируется ни один файл?

                                                Например если задана переменная окружения $SRC_DIR или пуст файл со списком для проверки
                                                  0
                                                  Файлы сканируются — это видно при включении лога.
                                                  Содержимое Stats.us выложил здесь, вместе с примером анализа: habrahabr.ru/qa/23410/
                                                    0
                                                    ответил там

                                              Only users with full accounts can post comments. Log in, please.