Перейти к содержимому

Поиск по AST

В режиме AST Greph ищет в исходном коде PHP по структуре, а не по тексту. Шаблоны записываются как обычный код на PHP с $VARIABLE и $$$VARIADIC метапеременными. Шаблон разбирается с помощью nikic/php-parser в ту же структуру AST, что и исходный код, по которому выполняется поиск, а затем сопоставляется узел за узлом.

Это та же модель, что и у ast-grep, а оболочка совместимости sg предоставляет интерфейс CLI из ast-grep поверх того же механизма.

Краткие примеры

Командная оболочка
# Find every constructor call
./vendor/bin/greph -p 'new $CLASS()' src

# Find every method call with any number of arguments
./vendor/bin/greph -p '$obj->$method($$$ARGS)' src

# Find old-style array() calls
./vendor/bin/greph -p 'array($$$ITEMS)' src

# Find void-returning functions
./vendor/bin/greph -p 'function $name($$$PARAMS): void {}' src

# Find isset ternaries that should be ??
./vendor/bin/greph -p 'isset($x) ? $x : $default' src

Синтаксис шаблонов

Шаблон — это допустимый исходный код PHP. Там, где может стоять настоящее выражение или инструкция, можно подставить метапеременную, которая захватит этот узел.

Синтаксис Совпадает с Пример
$VAR Любой одиночный узел AST, захваченный как VAR $x + $y совпадает с foo() + bar
$_ Любой одиночный узел AST без захвата $_->method() совпадает с любым получателем
$$$ARGS Ноль или более узлов (вариативный набор), захваченных как ARGS func($$$ARGS) совпадает с func(), func(1), func(1, 2, 3)
$VAR (повторная) Одна и та же $VAR должна оба раза совпадать с одинаковой структурой $x == $x совпадает с a == a, но не с a == b

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

$$$VARIADIC работает везде, где PHP допускает список узлов: аргументы функции, элементы массива, списки параметров, тела инструкций. Захват сохраняет всю найденную последовательность, включая ноль элементов.

Метапеременные идентификаторов

Имена классов, функций, интерфейсов, трейтов и перечислений — это идентификаторы, а не выражения, поэтому обычно PHP не принимает $Name после class. Greph переписывает такие шаблоны перед разбором, чтобы метапеременная работала:

Командная оболочка
./vendor/bin/greph -p 'class $NAME extends BaseController {}' src
./vendor/bin/greph -p 'function $NAME(): void {}' src
./vendor/bin/greph -p 'interface $NAME {}' src

То же переписывание выполняется для class, interface, trait, enum и function.

Повторные метапеременные

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

Командная оболочка
# Tautological comparisons
./vendor/bin/greph -p '$X == $X' src

# Redundant ternaries
./vendor/bin/greph -p '$X ? $X : null' src

То же правило действует для $$$VARIADIC: оба вхождения $$$ARGS должны совпадать с одной и той же последовательностью.

Фильтрация файлов

Режим AST обходит дерево файлов тем же способом, что и текстовый режим, и по умолчанию использует фильтр типов файлов php. Переопределите его с помощью --type, --type-not, --glob, --no-ignore и --hidden. Обход по-прежнему учитывает .gitignore и .grephignore и пропускает двоичные файлы.

Командная оболочка
./vendor/bin/greph -p 'new $CLASS()' --glob 'src/**/*.php' .
./vendor/bin/greph -p 'new $CLASS()' --type-not phpt src

Вывод

По умолчанию используется тот же формат file:line:content, что и в текстовом режиме; найденный код сворачивается в одну строку:

Text
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();

Передайте --json, чтобы вывести структурированные совпадения:

JSON
[
  {
    "file": "src/Greph.php",
    "start_line": 38,
    "end_line": 38,
    "start_file_pos": 1234,
    "end_file_pos": 1252,
    "code": "new FileWalker()"
  }
]

Ошибки разбора

По умолчанию Greph молча пропускает файлы, которые не удается разобрать. Это удобно при работе с крупной кодовой базой, содержащей экспериментальные или сгенерированные файлы PHP. Передайте --strict-parse (для greph-index ast-index search и greph-index ast-cache search), чтобы вместо этого вызвать ошибку разбора, или задайте skipParseErrors: false для AstSearchOptions при непосредственном использовании фасада.

Программное использование

PHP
use Greph\Greph;
use Greph\Ast\AstSearchOptions;

$matches = Greph::searchAst(
    '$obj->$method($$$ARGS)',
    'src',
    new AstSearchOptions(jobs: 4),
);

foreach ($matches as $match) {
    echo "{$match->file}:{$match->startLine}\n";
    echo $match->code . "\n";

    foreach ($match->captures as $name => $node) {
        echo "  {$name} = " . get_debug_type($node) . "\n";
    }
}

$match->node — это захваченный PhpParser\Node (поэтому его можно обойти с помощью NodeVisitor из PHP-Parser), а $match->captures сопоставляет каждое имя метапеременной с захваченным узлом или списком узлов.

Как это работает

Конвейер состоит из следующих этапов:

  1. Разбор шаблона: Greph\Ast\PatternParser переписывает метапеременные идентификаторов, затем разбирает шаблон с помощью PHP-Parser.
  2. Разбор исходного кода: каждый файл-кандидат разбирается один раз. В режиме ast-cache этот этап заменяется десериализацией из кэшированного хранилища.
  3. Фильтрация кандидатов: Greph\Ast\AstPatternPrefilter извлекает из шаблона грубую сигнатуру (имя вызова, имя класса, тип инструкции) и использует ее, чтобы пропускать файлы, в которых заведомо не может быть совпадений. В режиме ast-index вместо этого этапа запрашивается предварительно вычисленное хранилище фактов.
  4. Сопоставление: Greph\Ast\PatternMatcher обходит AST исходного кода и сравнивает его с AST шаблона, попутно связывая метапеременные и проверяя согласованность повторных переменных.
  5. Вывод результатов: найденные узлы преобразуются в объекты AstMatch с позициями строк и байтов, а также исходным фрагментом кода.

При повторяющихся задачах индексированный поиск по AST и поиск по кэшированному AST пропускают большую часть этапов 2 и 3.