Поиск по 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, что и в текстовом режиме; найденный код сворачивается в одну строку:
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();
Передайте --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 при непосредственном использовании фасада.
Программное использование
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 сопоставляет каждое имя метапеременной с захваченным узлом или списком узлов.
Как это работает
Конвейер состоит из следующих этапов:
- Разбор шаблона:
Greph\Ast\PatternParserпереписывает метапеременные идентификаторов, затем разбирает шаблон с помощью PHP-Parser. - Разбор исходного кода: каждый файл-кандидат разбирается один раз. В режиме
ast-cacheэтот этап заменяется десериализацией из кэшированного хранилища. - Фильтрация кандидатов:
Greph\Ast\AstPatternPrefilterизвлекает из шаблона грубую сигнатуру (имя вызова, имя класса, тип инструкции) и использует ее, чтобы пропускать файлы, в которых заведомо не может быть совпадений. В режимеast-indexвместо этого этапа запрашивается предварительно вычисленное хранилище фактов. - Сопоставление:
Greph\Ast\PatternMatcherобходит AST исходного кода и сравнивает его с AST шаблона, попутно связывая метапеременные и проверяя согласованность повторных переменных. - Вывод результатов: найденные узлы преобразуются в объекты
AstMatchс позициями строк и байтов, а также исходным фрагментом кода.
При повторяющихся задачах индексированный поиск по AST и поиск по кэшированному AST пропускают большую часть этапов 2 и 3.